#region Copyright (c) 2007-2008, Chris Klochek

/********************************************************************************************************************
'
' Copyright (c) 2007-2008, Chris Klochek
' All rights reserved.
'
' Redistribution and use in source and binary forms, with or without modification, are permitted provided
' that the following conditions are met:
'
' * Redistributions of source code must retain the above copyright notice, this list of conditions and the
' 	following disclaimer.
'
' * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and
' 	the following disclaimer in the documentation and/or other materials provided with the distribution.
'
' * Neither the name of the author nor the names of its contributors may be used to endorse or
' 	promote products derived from this software without specific prior written permission.
'
' THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
' WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
' PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
' ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
' LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
' INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
' OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
' IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
'
'*******************************************************************************************************************/

#endregion

using System;
using System.IO;
using System.Reflection;
using System.Windows.Forms;
using System.Collections.Generic;
using System.Drawing;
using Extensibility;
using EnvDTE;
using EnvDTE80;
using Microsoft.Win32;
using System.Threading;

using SuedeTest.Core;

using System.Runtime.Remoting.Channels.Ipc;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Lifetime;
using SuedeTest.Core.Actions;

namespace SuedeTest.VSPlugin {
   /// <summary>
   /// The main form for the Visual Studio Plugin
   /// </summary>
   public partial class AppForm : UserControl {
      private const string STARTUP_PROJECT = "StartupProject";
      private const string PROJECT_SUFFIX = ".csproj";
      private const string FULL_PATH = "FullPath";
      private const string OUTPUT_PATH = "OutputPath";
      private const string OUTPUT_FILE_NAME = "OutputFileName";

      private const string PROCESS_HOST = "SuedeTest.ProcessHost.exe";
      private const string TEMP_EXE_NAME = "__oldexe.exe";

      private const string IPC_PROTOCOL_NAME = "ipc";
      private const string IPC_PORT_NAME = "SuedeTest";
      private const string IPC_OBJECT_URI = "TextBridge";

      internal const string VS_PLUGIN_REGKEY = @"Software\SuedeTest\";
      internal const string LAST_TEST_DIRECTORY_REGKEY = @"TestDirectory";
      internal const string LAST_ASSEMBLY_DIRECTORY = @"AssembliesDirectory";

      private TestWriter _testWriter;

      private IpcChannel _ipcChannel;
      private SuedeTest.ProcessHost.ProcessLink _processBridge;

      private System.Diagnostics.Process _testingProcess;

      private string _baseTestDirectory = "";
      private string _additionalAssembliesDir = "";

      private RegistryKey _pluginKey;

      /// <summary>
      /// Initialize the appform; create the registry key the plugin uses
      /// </summary>
      public AppForm() {
         InitializeComponent();

         _pluginKey = Registry.CurrentUser.CreateSubKey(VS_PLUGIN_REGKEY);

         _baseTestDirectory = GetRegKeyValue(LAST_TEST_DIRECTORY_REGKEY);
         _additionalAssembliesDir = GetRegKeyValue(LAST_ASSEMBLY_DIRECTORY);

         _testWriter = new TestWriter_FIT();

         AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
      }

      private string GetIpcUrl() {
         return IPC_PROTOCOL_NAME + "://" + IPC_PORT_NAME + "/" + IPC_OBJECT_URI;
      }

      /// <summary>
      /// Allows the user to select a directory that contain additional assemblies
      /// </summary>
      /// <param name="sender"></param>
      /// <param name="e"></param>
      private void btnCustomControls_Click(object sender, EventArgs e) {
         FolderBrowserDialog fbd = new FolderBrowserDialog();

         string lastDir = GetRegKeyValue(LAST_ASSEMBLY_DIRECTORY);

         if (!string.IsNullOrEmpty(lastDir)) {
            fbd.SelectedPath = lastDir;
         }

         if (fbd.ShowDialog() == DialogResult.OK) {
            SetRegKeyValue(LAST_ASSEMBLY_DIRECTORY, fbd.SelectedPath);
            _additionalAssembliesDir = fbd.SelectedPath;
         }
      }

      /// <summary>
      /// User has selected a new tab
      /// </summary>
      /// <param name="sender"></param>
      /// <param name="e"></param>
      private void tabControl1_SelectedIndexChanged(object sender, EventArgs e) {
         if (tabControl1.SelectedTab.Text.Equals("Run Tests")) {
            string dir = GetRegKeyValue(LAST_TEST_DIRECTORY_REGKEY);

            if (dir != null) {
               LoadTests(dir);
            }
         }
      }

      /// <summary>
      /// Occurs after checking a node in the test-tree hierarchy
      /// </summary>
      /// <param name="sender"></param>
      /// <param name="e"></param>
      private void trTestTree_AfterCheck(object sender, TreeViewEventArgs e) {
         trTestTree.BeginUpdate();
         TreeNode trc = e.Node;
         foreach (TreeNode tn in e.Node.Nodes) {
            tn.Checked = e.Node.Checked;
         }
         trTestTree.EndUpdate();
      }

      /// <summary>
      /// Called when the run button is clicked.
      /// </summary>
      /// <param name="sender"></param>
      /// <param name="e"></param>
      private void btnRun_Click(object sender, EventArgs e) {
         txtTestOutput.Clear();

         //List<string> testNames = CollectTestNames();

         tabControl1.SelectTab("tabTestOutput");
         foreach (TreeNode tr in trTestTree.Nodes) {
            RunCheckedTests(tr);
         }
      }

      private string GetAdditionalLibsAsCLArg() {
         if (!string.IsNullOrEmpty(_additionalAssembliesDir)) {
            return "-libs " + _additionalAssembliesDir;
         }
         return "";
      }

      private string GetTestFileAsCLArg(string testFileName) {
         if (!string.IsNullOrEmpty(testFileName)) {
            return "-testfile " + testFileName;
         }
         return "";
      }

      private string GetTestNamesAsCLArg(string[] testNames) {
         string result = "";
         if (testNames != null && testNames.Length > 0) {
            for (int x = 0; x < testNames.Length; x++) {
               if (!string.IsNullOrEmpty(testNames[x])) {
                  result += testNames[x] + " ";
               }
            }

            result = result.Trim();
            if (result != string.Empty) {
               return "-tests " + result;
            }
         }
         return "";
      }

      /// <summary>
      /// Runs a test from within a test-file.
      /// </summary>
      /// <param name="testFileName">Name of the .html file containing the test.</param>
      /// <param name="testName">Name of the test itself.</param>
      private void RunTest(string testFileName, string[] testNames) {

         EnsureProcessDestroyed();

         CreateIPCBridge();

         _processBridge.LogUpdated += new SuedeTest.ProcessHost.ProcessLink.LogUpdatedDelegate(OnLogUpdated);

         string args = GetIpcUrl() + " " +
         GetAdditionalLibsAsCLArg() + " " +
         GetTestFileAsCLArg(testFileName) + " " +
         GetTestNamesAsCLArg(testNames);

         LaunchProcess(PROCESS_HOST, args.Trim());
      }

      void OnLogUpdated(string newText) {
            txtTestOutput.Invoke((MethodInvoker)delegate() {
               txtTestOutput.Text = txtTestOutput.Text + newText;
               txtTestOutput.SelectAll();
               txtTestOutput.ScrollToCaret();
            });
      }

      /// <summary>
      /// Runs a test from the TreeNode tr.  Parent of tr must be the test file.
      /// </summary>
      /// <param name="tr">Tree node with text containing the test name</param>
      private void RunCheckedTests(TreeNode tr) {
         //check it's a file, and not a directory
         if (File.Exists(_baseTestDirectory + BuildFullDirName(tr))) {
            if (tr.Text.EndsWith(".html")) {
               if (tr.GetNodeCount(false) != 0) {
                  List<string> testNames = new List<string>();

                  foreach (TreeNode child in tr.Nodes) {
                     if (child.Checked) {
                        testNames.Add(child.Text);
                     }
                  }

                  if (testNames.Count > 0) {
                     RunTest(_baseTestDirectory + BuildFullDirName(tr), testNames.ToArray());
                  }
               }
            }
         } else {
            if (tr.GetNodeCount(false) != 0) {
               foreach (TreeNode trc in tr.Nodes) {
                  RunCheckedTests(trc);
               }
            }
         }
      }

      /// <summary>
      /// Double-click opens the html file in the editor.
      /// </summary>
      /// <param name="sender"></param>
      /// <param name="e"></param>
      private void trTestTree_NodeMouseDoubleClick(object sender, TreeNodeMouseClickEventArgs e) {
         try {
            string nodeName = e.Node.Text.ToLower();
            if (nodeName.EndsWith(".html") || nodeName.EndsWith(".htm")) {
               string fullPath = _baseTestDirectory + BuildFullDirName(e.Node);
               Connect._applicationObject.DTE.ItemOperations.OpenFile(fullPath, Constants.vsViewKindTextView);
            }
         } catch (Exception) { }
      }

      /// <summary>
      /// Called when the user clicks the Load Test button.
      /// </summary>
      /// <param name="sender"></param>
      /// <param name="e"></param>
      private void btnLoadTests_Click(object sender, EventArgs e) {
         FolderBrowserDialog fd = new FolderBrowserDialog();

         fd.Description = "Choose Test Folder";

         fd.ShowNewFolderButton = false;

         string lastDir = GetRegKeyValue(LAST_TEST_DIRECTORY_REGKEY);
         if (!string.IsNullOrEmpty(lastDir)) {
            fd.SelectedPath = lastDir;
         }

         if (fd.ShowDialog() == DialogResult.OK) {
            if (fd.SelectedPath != null) {
               SetRegKeyValue(LAST_TEST_DIRECTORY_REGKEY, fd.SelectedPath);
               LoadTests(fd.SelectedPath);
            }
         }
      }

      /// <summary>
      /// Load all tests recursively from the directory "directory", and place them in
      /// the test-tree.  "directory"'s children are the roots of the tree.
      /// </summary>
      /// <param name="directory"></param>
      private void LoadTests(string directory) {
         _baseTestDirectory = directory;
         TreeNode rootNode = new TreeNode();
         trTestTree.Nodes.Clear();
         trTestTree.Nodes.Add(rootNode);

         LoadTests(directory, rootNode);
      }

      /// <summary>
      /// Recursively build the full directory name of a test by appending it's name to
      /// it's parent's, and so on.
      /// </summary>
      /// <param name="node"></param>
      /// <returns></returns>
      private string BuildFullDirName(TreeNode node) {
         if (node.Parent != null) {
            if (node.Parent.Parent != null) {
               return BuildFullDirName(node.Parent) + "\\" + node.Text;
            } else {
               return "\\" + node.Text;
            }
         }
         //shouldn't actually happen, but let's fail gracefully
         return "";
      }

      /// <summary>
      /// Load a set of tests (recursively) from the directory "directory".
      /// The directory must be valid at this point.  All directories at "this"
      /// level will be loaded into the child-nodes of 'node'
      /// </summary>
      /// <param name="directory"></param>
      private void LoadTests(string directory, TreeNode node) {
         int lastIdx = directory.LastIndexOf('\\');
         if (lastIdx < 0) {
            lastIdx = 0;
         }

         node.Text = directory.Substring(lastIdx);

         DirectoryInfo dir = new DirectoryInfo(directory);

         FileInfo[] testFiles = dir.GetFiles();

         foreach (FileInfo f in testFiles) {

            string fileName = f.FullName.ToLower();
            if (fileName.EndsWith(".html") || fileName.EndsWith(".htm")) {
               //Add each Individual test from file
               TreeNode chNode = new TreeNode(f.Name);
               chNode.BackColor = Color.AliceBlue;
               node.Nodes.Add(chNode);

               TestProgram tp = new TestProgram(fileName, null);
               foreach (TestFrame frame in tp.TestFrames) {
                  chNode.Nodes.Add(frame.Name);
               }
            }
         }

         //get all child directories
         foreach (DirectoryInfo di in dir.GetDirectories()) {
            TreeNode chNode = new TreeNode();
            node.Nodes.Add(chNode);
            LoadTests(di.FullName, chNode);
         }

      }

      /// <summary>
      /// Gets a registry key value from the plugin registry key with name "keyname"
      /// </summary>
      /// <param name="keyName"></param>
      /// <returns></returns>
      private string GetRegKeyValue(string keyName) {
         return _pluginKey.GetValue(keyName) as string;
      }

      /// <summary>
      /// Sets a registry key value at the plugin registry key with name "keyname"
      /// and value "value"
      /// </summary>
      /// <param name="keyName"></param>
      /// <param name="value"></param>
      private void SetRegKeyValue(string keyName, string value) {
         if (!string.IsNullOrEmpty(value)) {
            _pluginKey.SetValue(keyName, value);
         }
      }

      /// <summary>
      /// Called when the user clicks the record button
      /// </summary>
      /// <param name="sender"></param>
      /// <param name="e"></param>
      private void btnRecord_Click(object sender, EventArgs e) {
         //if no solution is opened in IDE
         if (Connect._applicationObject.Solution == null) {
            return;
         }

         //if solution opened in IDE needs fresh build
         /*if (Connect._applicationObject.Solution.IsDirty) {
         Connect._applicationObject.Solution.SolutionBuild.Run();

         //if compile fails
         if (Connect._applicationObject.Solution.SolutionBuild.LastBuildInfo != 0) {
         return;  //won't run on an failed compile
         }
         }*/

         txtLogger.Clear();
         txtTestResults.Clear();

         RecordTest();
      }

      /// <summary>
      /// Records a test; the application is run in it's own process.  RecorderHelper is
      /// that process, which in turn launches the desired program.
      /// </summary>
      private void RecordTest() {
         EnsureProcessDestroyed();

         CreateIPCBridge();

         _processBridge.TestUpdated += TestUpdated;
         _processBridge.LogUpdated += new SuedeTest.ProcessHost.ProcessLink.LogUpdatedDelegate(_processBridge_LogUpdated);
         _processBridge.TestCleared += new EventHandler(_processBridge_TestCleared);

         _testWriter.TestChanged += UpdateTests;

         string args = GetIpcUrl() + " " +
         GetAdditionalLibsAsCLArg();

         LaunchProcess(PROCESS_HOST, args.Trim());
      }

      void _processBridge_TestCleared(object sender, EventArgs e) {
         _testWriter.Clear();
      }

      void _processBridge_LogUpdated(string newText) {
         txtLogger.Invoke((MethodInvoker)delegate()
         {
            txtLogger.Text += newText + Environment.NewLine;
            txtLogger.Select(txtLogger.TextLength, 0);
            txtLogger.ScrollToCaret();
         });
      }

      private void TestUpdated(SerializedAction testAction) {
         _testWriter.AddAction(testAction);
      }

      /// <summary>
      /// Called when a test-recording is updated
      /// </summary>
      public void UpdateTests(object sender, EventArgs args) { 
         if (txtTestResults.InvokeRequired) {
            txtTestResults.Invoke((MethodInvoker)delegate() {
               txtTestResults.Text = _testWriter.Test;
               txtTestResults.Select(txtTestResults.TextLength, 0);
               txtTestResults.ScrollToCaret();
            });
         } else {
            txtTestResults.Text = _testWriter.Test;
            txtTestResults.Select(txtTestResults.TextLength, 0);
            txtTestResults.ScrollToCaret();
         }
      }

      /// <summary>
      /// Launch a recorder/runner process, with the supplied arguments.
      /// Other standard args/bookkeeping that is common to launching procs occurs here.
      /// </summary>
      /// <param name="procName"></param>
      /// <param name="args"></param>
      private void LaunchProcess(string procName, string args) {
         string startupExeFullFileName = GetStartupProjectAssemblyName();
         string projDir = GetStartupProjectWorkingDir();
         string pluginLocation = Assembly.GetExecutingAssembly().Location;

         if (string.IsNullOrEmpty(startupExeFullFileName) ||
         string.IsNullOrEmpty(projDir) ||
         string.IsNullOrEmpty(pluginLocation)) {
            return;
         }

         try {
            pluginLocation = pluginLocation.Substring(0, pluginLocation.LastIndexOf(@"\"));

            //app.exe -> __oldexe.exe
            //procname.exe -> app.exe
            //we'll restore the names properly after the process has exited
            File.Copy(startupExeFullFileName, projDir + @"\" + TEMP_EXE_NAME, true);
            File.Copy(pluginLocation + @"\" + procName, startupExeFullFileName, true);

            _testingProcess = new System.Diagnostics.Process();
            _testingProcess.EnableRaisingEvents = true;
            _testingProcess.Exited += new EventHandler(ProcessExitedCallback);

            string procArgs = projDir + @"\" + TEMP_EXE_NAME + " " +
            pluginLocation + " " +
            args;

            _testingProcess.StartInfo = new System.Diagnostics.ProcessStartInfo(startupExeFullFileName, procArgs.Trim());
            _testingProcess.StartInfo.WorkingDirectory = projDir;
            _testingProcess.Start();
         } catch (Exception ex) {
            MessageBox.Show(ex.Message, "Unable to launch " + procName);
         }
      }

      /// <summary>
      /// Called when the testing process (recorder or runner) has exited.
      /// Restores original file names.
      /// </summary>
      /// <param name="sender"></param>
      /// <param name="e"></param>
      private void ProcessExitedCallback(object sender, EventArgs e) {
         _testingProcess.Close();
         _testingProcess.Dispose();
         _testingProcess = null;

         ShutdownIPC();

         RestoreFiles();
      }

      private void ShutdownIPC() {
         if (_ipcChannel != null) {
            ChannelServices.UnregisterChannel(_ipcChannel);
            _ipcChannel = null;
         }

         if (_processBridge != null) {
            _processBridge = null;
         }
      }

      private void EnsureProcessDestroyed() {
         if (_testingProcess != null) {
            if (!_testingProcess.HasExited) {
               try {
                  _testingProcess.Kill();
                  _testingProcess.Close();
                  _testingProcess.Dispose();
                  _testingProcess = null;
               } catch { }

               RestoreFiles();

               ShutdownIPC();
            }
         }
      }

      private void RestoreFiles() {
         string projDir = GetStartupProjectWorkingDir();

         try {
            File.Copy(projDir + @"\" + TEMP_EXE_NAME, GetStartupProjectAssemblyName(), true);
            File.Delete(projDir + @"\" + TEMP_EXE_NAME);
         } catch (Exception) { }
      }

      /// <summary>
      /// Creates an IPC channel for this process, hosting a single "textbridge" that other processes can
      /// post strings to.
      /// </summary>
      private void CreateIPCBridge() {
         BinaryServerFormatterSinkProvider serverProvider = new BinaryServerFormatterSinkProvider();
         serverProvider.TypeFilterLevel = System.Runtime.Serialization.Formatters.TypeFilterLevel.Full;
         System.Collections.IDictionary props = new System.Collections.Hashtable();
         props["name"] = IPC_PROTOCOL_NAME;
         props["portName"] = IPC_PORT_NAME;
         props["exclusiveAddressUse"] = "false";

         //todo: find out what the damn args are for this
         /*props["lifetime.leasetime"] = "10";
         props["lifetime.renewOnCallTime"] = "10";
         props["lifetime.sponsorshipTimeOut"] = "10";*/

         _ipcChannel = new IpcChannel(props, new BinaryClientFormatterSinkProvider(), serverProvider);

         ChannelServices.RegisterChannel(_ipcChannel, false);

         RemotingConfiguration.RegisterWellKnownServiceType(typeof(SuedeTest.ProcessHost.ProcessLink), IPC_OBJECT_URI, WellKnownObjectMode.Singleton);

         _processBridge = (SuedeTest.ProcessHost.ProcessLink)Activator.GetObject(typeof(SuedeTest.ProcessHost.ProcessLink), GetIpcUrl());

         ILease lease = (ILease)_processBridge.GetLifetimeService();
         lease.Renew(new TimeSpan(24, 0, 0));
      }

      /// <summary>
      /// .NET is very flaky when it comes to asm. resolution: in particular, if the app
      /// directory (in this case, Program Files/VS/IDE/etc...) doesn't match the executing
      /// assembly dir (in this case, c:/My Plugin/etc...), it will choke on resolving EVEN
      /// the currently executing assembly.  Provide this to help it out.
      /// </summary>
      /// <param name="sender"></param>
      /// <param name="args"></param>
      /// <returns></returns>
      Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) {
         foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies()) {
            if (asm.FullName == args.Name) {
               return asm;
            }
         }
         return null;
      }

      /// <summary>
      /// Gets the full name of the startup solution. (e.g. c:\project\bin\debug\project.exe)
      /// </summary>
      /// <returns></returns>
      private string GetStartupProjectAssemblyName() {
         string projName = "";

         System.Array startupProjects = (System.Array)Connect._applicationObject.Solution.SolutionBuild.StartupProjects;
         if (startupProjects != null & startupProjects.Length > 0) {
            projName = startupProjects.GetValue(0) as string;
         }

         if (!string.IsNullOrEmpty(projName)) {
            Project proj = Connect._applicationObject.Solution.Projects.Item(projName);
            string fullPath = proj.Properties.Item(FULL_PATH).Value as string;
            fullPath += proj.ConfigurationManager.ActiveConfiguration.Properties.Item(OUTPUT_PATH).Value as string;
            fullPath += proj.Properties.Item(OUTPUT_FILE_NAME).Value as string;
            return fullPath;
         }
         return "";
      }

      private string GetStartupProjectWorkingDir() {
         string fullProj = GetStartupProjectAssemblyName();
         return fullProj.Substring(0, fullProj.LastIndexOf("\\"));
      }

      private string GetStartupProjectFileName() {
         string fullProj = GetStartupProjectAssemblyName();
         return fullProj.Substring(fullProj.LastIndexOf("\\") + 1);
      }
   }
}